/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Library General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor Boston, MA 02110-1301,  USA
 */

/**
 * @version login.c 0.0.1
 * @author Dr.NP <np@bsgroup.org>
 * @package Comoro
 *
 * Login server
 */

#include "bspd.h"

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <time.h>
#include <event.h>
#include <pthread.h>

#ifdef HAVE_MALLOC_H
#ifndef __OpenBSD__
#include <malloc.h>
#endif
#endif

extern struct conf setting;
extern struct event_base *ev_base;
static unsigned long inited_time;
struct user_session session_list[MAX_CLIENT];
struct base_info base_list[MAX_LOGIN_BASE_SERVER];
pthread_mutex_t db_lock;

/* Login server initialize */
int login_init()
{
	// MySQL init
	if (0 != db_mysql_init() || 0 != db_mysql_create_env(0))
	{
		fprintf(stderr, "Database initialized error\n");
		exit(EXIT_DB_ERROR);
	}

	// Socket server init
	struct conn_server *c = (struct conn_server *) malloc(sizeof(struct conn_server));

	memset(c, 0, sizeof(struct conn_server));
	
	c->srv_addr = inet_addr(setting.login_srv_listen_addr);
	c->srv_port = setting.login_srv_port;
	c->listen_queue_length = 4096;
	c->nonblock = 1;
	c->tcp_nodelay = 1;
	c->tcp_reuse = 1;
	c->tcp_send_buffer_size = 131072;
	c->tcp_recv_buffer_size = 131072;
	c->tcp_timeout = 3;
	c->on_data_callback = login_on_data;
	c->on_conn_callback = login_on_conn;
	c->on_close_callback = login_on_close;

	// A new TCP server
	conn_tcp_server_new(c);

	// Status server init
	struct conn_server *s = (struct conn_server *) malloc(sizeof(struct conn_server));

	memset(s, 0, sizeof(struct conn_server));

	s->srv_addr = inet_addr(setting.login_srv_listen_addr);
	s->srv_port = setting.login_srv_status_port;
	s->listen_queue_length = 4096;
	s->nonblock = 1;
	s->tcp_nodelay = 1;
	s->tcp_reuse = 1;
	s->tcp_send_buffer_size = 131072;
	s->tcp_recv_buffer_size = 131072;
	s->tcp_timeout = 3;
	s->on_data_callback = login_on_status_data;
	s->on_conn_callback = login_on_status_reg;
	s->on_close_callback = login_on_status_unreg;

	conn_tcp_server_new(s);

	// Session list init
	memset(session_list, 0, sizeof(struct user_session) * MAX_CLIENT);
	memset(base_list, 0, sizeof(struct base_info) * MAX_LOGIN_BASE_SERVER);

	inited_time = (unsigned long) time((time_t *) NULL);

	return RTN_SUCCESS;
}

/* Callback when data arrival */
void login_on_data(int client_id, void *data, unsigned int len)
{
	char l_username[MAX_USERNAME_LENGTH];
	char l_password[MAX_PASSWORD_LENGTH];
	unsigned int data_remain = len;
	unsigned int plen;
	unsigned int spa;
	
	// Flash security request
	if (0 == strncmp("<policy-file-request/>", data, 22))
	{
		conn_client_send(conn_client_list_get(client_id), setting.srv_sandbox_str, -1);
		
		return;
	}
	
	//package_output(data, len);
	// Split data into commands
	while(data_remain > 0)
	{
		if (data_remain >= 4)
		{
			// Read package length
			plen = get_u_int(data + len - data_remain);
			if (plen > SGN_RANGE)
			{
				// Login
				if (plen == SGN_CMD_LOGIN && data_remain >= MAX_USERNAME_LENGTH + MAX_PASSWORD_LENGTH + 8)
				{
					strncpy(l_username, data + 4, MAX_USERNAME_LENGTH);
					strncpy(l_password, data + 4 + MAX_USERNAME_LENGTH, MAX_PASSWORD_LENGTH);
					spa = get_u_int(data + 4 + MAX_USERNAME_LENGTH + MAX_PASSWORD_LENGTH);

					login_do_login(client_id, l_username, l_password, (int) spa);
					data_remain -= MAX_USERNAME_LENGTH + MAX_PASSWORD_LENGTH + 8;

					continue;
				}
				
				// Special command
				else if (data_remain >= 8)
				{
					// 8 bytes per command
					spa = get_u_int(data + len - data_remain + 4);
					login_special_command(client_id, plen, spa);
					data_remain -= 8;

					continue;
				}

				else
				{
					// Command error
					data_remain = 0;
					
					break;
				}
			}

			else
			{
				data_remain = 0;
				
				break;
			}
		}
	}
	
	return;
}

/* Call back when client connect */
void login_on_conn(int client_id)
{
	if (client_id > MAX_CLIENT)
	{
		return;
	}
	
	// Clear session
	memset(&session_list[client_id], 0, sizeof(struct user_session));
	
	return;
}

/* Call back when client offline */
void login_on_close(int client_id)
{
	if (client_id > MAX_CLIENT)
	{
		return;
	}
	
	// Clear session
	memset(&session_list[client_id], 0, sizeof(struct user_session));
	
	return;
}

/* Call back when base server info */
void login_on_status_data(int srv_id, void *data, unsigned int len)
{
	unsigned int data_remain = len;
	unsigned int plen;
	unsigned int addr;
	unsigned int port;
	unsigned int spa;
	unsigned long long user_id;
	static char signal_r[8];

	// Split data into commands
	while(data_remain > 0)
	{
		if (data_remain >= 4)
		{
			plen = get_u_int(data + len - data_remain);

			if (plen == SGN_BASE_REG && data_remain >= 12)
			{ 
				// Base server reg
				addr = get_u_int(data + len - data_remain + 4);
				port = get_u_int(data + len - data_remain + 8);

				login_reg_base(srv_id, (unsigned long) addr, (int) port);
				
				data_remain -= 12;
			}

			else if (plen == SGN_BASE_INFO && data_remain >= 8)
			{
				// Client re-count
				spa = get_u_int(data + len - data_remain + 4);
				
				if (srv_id >= 0 && srv_id < MAX_LOGIN_BASE_SERVER)
				{
					if (base_list[srv_id].srv_port)
					{
						base_list[srv_id].num_clients = (int) spa;
					}
				}
				
				data_remain -= 8;
			}

			else if (plen == SGN_SESSION_DATA && data_remain >= 12)
			{
				// Session arrival?
				user_id = get_u_ll(data + len - data_remain + 4);

				for (spa = 0; spa < MAX_CLIENT; spa ++)
				{
					if (session_list[spa].user_id == user_id && strlen(session_list[spa].username))
					{
						// Find server
						put_u_int(SGN_CMD_CONN, signal_r);
						put_u_int(0, signal_r + 4);

						conn_client_send(conn_client_list_get(spa), signal_r, 8);
						
						break;
					}
				}
				
				data_remain -= 12;
			}

			else if (plen == SGN_HEARTBEAT && data_remain >= 8)
			{
				conn_client_hb(conn_client_list_get(srv_id));

				data_remain -= 8;
			}

			else
			{
				data_remain = 0;
				
				break;
			}
		}
	}
	
	return;
}

/* Call back when base server reg */
void login_on_status_reg(int srv_id)
{
	return;
}

/* Call back when base server unreg */
void login_on_status_unreg(int srv_id)
{
	if (srv_id >= 0 && srv_id < MAX_LOGIN_BASE_SERVER)
	{
		base_list[srv_id].base_id = 0;
		base_list[srv_id].srv_addr = 0;
		base_list[srv_id].srv_port = 0;
		base_list[srv_id].num_clients = 0;

		fprintf(stderr, "Base server %d unregistered\n", srv_id);
		log_add("Base server %d unregistered", srv_id);
	}
	
	return;
}

/* Special command (Command ID > 0xFFFFFF00) */
int login_special_command(int client_id, unsigned int cmd_id, unsigned int cmd_val)
{
	static char signal_s[16 + MAX_USERNAME_LENGTH + SESSION_KEY_LENGTH];
	static char signal_l[32];
	
	if (SGN_HEARTBEAT == cmd_id && 0x0 == cmd_val)
	{
		// Heartbeat
		conn_client_hb(conn_client_list_get(client_id));
	}

	else if (SGN_CMD_BASE_LIST == cmd_id && 0x0 == cmd_val)
	{
		// Get base server list
		int i;

		for (i = 0; i < MAX_LOGIN_BASE_SERVER; i ++)
		{
			if (base_list[i].srv_port)
			{
				// Available server
				put_u_int(SGN_CMD_BASE_LIST, signal_l);
				put_u_int((unsigned int) i, signal_l + 4);
				put_u_int((unsigned int) base_list[i].srv_addr, signal_l + 8);
				put_u_int((unsigned int) base_list[i].srv_port, signal_l + 12);
				put_u_int((unsigned int) base_list[i].num_clients, signal_l + 16);

				conn_client_send(conn_client_list_get(client_id), signal_l, 20);
			}
		}

		put_u_int(SGN_CMD_BASE_LIST, signal_l);
		put_u_int(0xFFFFFFFF, signal_l + 4);

		conn_client_send(conn_client_list_get(client_id), signal_l, 20);
	}

	else if (SGN_CMD_CONN == cmd_id)
	{
		// Transfer session to target server
		if (client_id < 0 || client_id >= MAX_CLIENT || cmd_val < 0 || cmd_val >= MAX_LOGIN_BASE_SERVER)
		{
			return RTN_GENERAL;
		}

		pthread_mutex_lock(&db_lock);
		
		if (strlen(session_list[client_id].username) > 0 && base_list[cmd_val].srv_port)
		{
			put_u_int(SGN_SESSION_DATA, signal_s);
			put_u_ll(session_list[client_id].user_id, signal_s + 4);
			memcpy(signal_s + 12, session_list[client_id].username, MAX_USERNAME_LENGTH);
			memcpy(signal_s + 12 + MAX_USERNAME_LENGTH, session_list[client_id].session_key, SESSION_KEY_LENGTH);
			put_u_int(session_list[client_id].app_id, signal_s + 12 + MAX_USERNAME_LENGTH + SESSION_KEY_LENGTH);

			conn_client_send(conn_client_list_get(cmd_val), signal_s, 16 + MAX_USERNAME_LENGTH + SESSION_KEY_LENGTH);
		}

		pthread_mutex_unlock(&db_lock);
	}

	return RTN_SUCCESS;
}

/* Do login process */
int login_do_login(int client_id, char *u, char *p, int app_id)
{
	struct mysql_row **row;
	static char query[1024];
	static char session_key[SESSION_KEY_LENGTH];
	static char signal[8 + SESSION_KEY_LENGTH];
	int nc = 0;
	char need_login = 0x0;
	long long user_id = 0;

	pthread_mutex_lock(&db_lock);

	// If app set need_login to false, client can login anonymously
	sprintf(query, "SELECT %s FROM %s WHERE %s = '%d'",
			setting.mysql_rlogin_column, 
	    	setting.mysql_runner_table, 
	    	setting.mysql_rid_column, 
	    	app_id);

	if (0 == db_mysql_query(0, query))
	{
		row = db_mysql_fetch_row(0, &nc);
	}

	else
	{
		fprintf(stderr, "Query error:%s\n", query);

		return RTN_DB_ERROR;
	}

	if (1 == nc)
	{
		// Query successfully
		if (row[0]->value && atoi(row[0]->value))
		{
			// Need login
			need_login = 0x1;
		}
	}

	db_mysql_del_row(row, nc);

	if (need_login)
	{
		// Query login
		sprintf(query, "SELECT %s FROM %s WHERE %s = '%s' AND %s = '%s' AND %s = '%d'", 
				setting.mysql_uid_column, 
				setting.mysql_user_table, 
				setting.mysql_uname_column, 
				u, 
				setting.mysql_upass_column, 
				p, 
				setting.mysql_urid_column, 
				app_id);

		if (0 == db_mysql_query(0, query))
		{
			row = db_mysql_fetch_row(0, &nc);

			if (nc == 1)
			{
				user_id = (row && row[0]->value) ? atoll(row[0]->value) : 0;
			}
		}

		else
		{
			fprintf(stderr, "Query error:%s\n", query);
		
			return RTN_DB_ERROR;
		}

		db_mysql_del_row(row, nc);
	}

	// Store session
	if (client_id < MAX_CLIENT)
	{
		package_unique_id(session_key, SESSION_KEY_LENGTH);
		session_list[client_id].client_id = client_id;
		session_list[client_id].user_id = user_id;
		memcpy(session_list[client_id].username, u, MAX_USERNAME_LENGTH);
		memcpy(session_list[client_id].session_key, session_key, SESSION_KEY_LENGTH);
		session_list[client_id].app_id = app_id;

		put_u_int(SGN_CMD_LOGIN, signal);
		put_u_int(nc, signal + 4);
		memcpy(signal + 8, session_key, SESSION_KEY_LENGTH);
		
		conn_client_send(conn_client_list_get(client_id), signal, 8 + SESSION_KEY_LENGTH);
	}

	pthread_mutex_unlock(&db_lock);
	
	return RTN_SUCCESS;
}

/* Base server registion */
int login_reg_base(int base_id, unsigned long addr, int port)
{
	if (base_id >= 0 && base_id < MAX_LOGIN_BASE_SERVER)
	{
		base_list[base_id].base_id = base_id;
		base_list[base_id].srv_addr = addr;
		base_list[base_id].srv_port = port;
		base_list[base_id].num_clients = 0;

		fprintf(stderr, "Base server %d registered\n", base_id);
		log_add("Base server %d registered", base_id);

		return base_id;
	}
	
	return RTN_GENERAL;
}
